go to index

Working with External Libraries

read time 21 min read
Kaggle Python

Working with External Libraries

在本教程中,你将学习Python中的imports(导入),获取一些使用不熟悉的库(以及它们返回的对象)的技巧,并深入研究operator overloading(运算符重载)

Imports

到目前为止,我们已经讨论了语言内置的类型和函数。

但Python最大的好处之一(尤其是对于数据科学家而言)是有大量高质量的自定义库为它编写。

其中一些库位于“标准库”中,这意味着你可以在运行Python的任何地方找到它们。可以轻松添加其他库,即使它们并不总是与Python一起提供。

无论哪种方式,我们都将通过**imports(导入)**来访问这段代码。

我们将从从标准库导入math开始我们的例子。

python
import math

print("It's math! It has type {}".format(type(math)))
plaintext
It's math! It has type <class 'module'>

math是一个module(模块)。模块只是其他人定义的变量的集合(你也可以称之为命名空间)。我们可以使用内置函数dir()查看数学中的所有名称。

python
print(dir(math))
plaintext
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'pi', 'pow', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc']

可以使用点语法访问这些变量。其中一些指向简单值,如 math.pi

python
print("pi to 4 significant digits = {:.4}".format(math.pi))
plaintext
pi to 4 significant digits = 3.142

但在这个模块中,我们能找到的大多是函数,比如math.log:

python
math.log(32, 2)
plaintext
5.0

当然,如果我们不知道math.log做了什么,可以对它调用help():

python
help(math.log)
python
Help on built-in function log in module math:

log(...)
    log(x, [base=math.e])
    Return the logarithm of x to the given base.
    
    If the base not specified, returns the natural logarithm (base e) of x.

我们也可以在模块本身上调用help()。这将为我们提供模块中所有函数和值的组合文档(以及模块的高级描述)。

python
help(math)

sorry 有点长我懒得写,你自己开个jupyer看看吧~

Other import syntax

如果我们知道将经常在math中使用函数,可以使用更短的别名导入它,以节省输入时间(尽管在本例中“math”已经很短了)。

python
import math as mt
mt.pi
plaintext
3.141592653589793

您可能已经看到过使用某些流行的库(如Pandas、Numpy、Tensorflow或Matplotlib)实现此功能的代码。例如,通常import numpy as np and import pandas as pd.

as只是重命名导入的模块。它等价于这样做:

python
import math
mt = math

如果我们可以自己引用math模块中的所有变量,不是很好吗? 例如,如果我们可以只引用pi而不是math.pi=或者mt.pi?好消息是:我们可以做到。

python
from math import *
print(pi, log(32, 2))
plaintext
3.141592653589793 5.0

Import * 使你可以直接访问模块中的所有变量(无需任何虚线前缀)。

坏消息:一些纯粹主义者可能会抱怨你这么做。

更糟糕的是:他们有点道理。

python
from math import *
from numpy import *
print(pi, log(32, 2))
python
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_19/3018510453.py in <module>
      1 from math import *
      2 from numpy import *
----> 3 print(pi, log(32, 2))

TypeError: return arrays must be of ArrayType

发生了什么事?它以前明明好好的!

这种“star imports(from module import *)”偶尔会导致奇怪、难以调试的情况。

这里的问题在于math和numpy模块都有log函数,但它们具有不同的语义。因为我们第二次从numpy导入了log函数,所以它的log会覆盖(或“投影”)我们从math中导入的log变量。

一个比较好的折中方案是只从每个模块中导入我们需要的特定内容:

python
from math import log, pi
from numpy import asarray

Submodules

我们已经看到,模块包含可以引用函数或值的变量。需要注意的是,它们也可以有变量引用其他模块。

python
import numpy
print("numpy.random is a", type(numpy.random))
print("it contains names such as...",
      dir(numpy.random)[-15:]
     )
plaintext
numpy.random is a <class 'module'>
it contains names such as... ['seed', 'set_state', 'shuffle', 'standard_cauchy', 'standard_exponential', 'standard_gamma', 'standard_normal', 'standard_t', 'test', 'triangular', 'uniform', 'vonmises', 'wald', 'weibull', 'zipf']

因此,如果我们像上面那样导入numpy,那么调用random “submodule” 中的函数将需要两个点。

python
# Roll 10 dice
rolls = numpy.random.randint(low=1, high=6, size=10)
rolls
plaintext
array([3, 4, 3, 4, 5, 5, 2, 1, 3, 3])

Oh the places you’ll go, oh the objects you’ll see

因此,在上了6节课之后,你就可以熟练使用整型、浮点型、布尔型、列表、字符串和字典(对吧?)

即使这是真的,事情也不会就此结束。当你使用各种库来完成特定任务时,你会发现它们定义了自己的类型,你必须学会使用这些类型。例如,如果你使用图形库matplotlib,你将接触到它定义的表示子图、图形、标记和注释的对象。pandas函数将提供数据框和序列。

在这一节中,我想与你分享一个使用奇怪类型的快速生存指南。

Three tools for understanding strange objects

在上面的单元格中,我们看到调用numpy函数会得到一个“array”。我们以前从未见过这样的东西(至少不是在本课程中)。但不要惊慌:我们有三个熟悉的内置函数来帮助我们。

1: type() (what is this thing?)

python
type(rolls)
plaintext
numpy.ndarray

2: dir() (what can I do with it?)

python
print(dir(rolls))
plaintext
['T', '__abs__', '__add__', '__and__', '__array__', '__array_finalize__', '__array_function__', '__array_interface__', '__array_prepare__', '__array_priority__', '__array_struct__', '__array_ufunc__', '__array_wrap__', '__bool__', '__class__', '__complex__', '__contains__', '__copy__', '__deepcopy__', '__delattr__', '__delitem__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__iand__', '__ifloordiv__', '__ilshift__', '__imatmul__', '__imod__', '__imul__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__ior__', '__ipow__', '__irshift__', '__isub__', '__iter__', '__itruediv__', '__ixor__', '__le__', '__len__', '__lshift__', '__lt__', '__matmul__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmatmul__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__setitem__', '__setstate__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__xor__', 'all', 'any', 'argmax', 'argmin', 'argpartition', 'argsort', 'astype', 'base', 'byteswap', 'choose', 'clip', 'compress', 'conj', 'conjugate', 'copy', 'ctypes', 'cumprod', 'cumsum', 'data', 'diagonal', 'dot', 'dtype', 'dump', 'dumps', 'fill', 'flags', 'flat', 'flatten', 'getfield', 'imag', 'item', 'itemset', 'itemsize', 'max', 'mean', 'min', 'nbytes', 'ndim', 'newbyteorder', 'nonzero', 'partition', 'prod', 'ptp', 'put', 'ravel', 'real', 'repeat', 'reshape', 'resize', 'round', 'searchsorted', 'setfield', 'setflags', 'shape', 'size', 'sort', 'squeeze', 'std', 'strides', 'sum', 'swapaxes', 'take', 'tobytes', 'tofile', 'tolist', 'tostring', 'trace', 'transpose', 'var', 'view']
python
# If I want the average roll, the "mean" method looks promising...
rolls.mean()
plaintext
3.3
python
# Or maybe I just want to turn the array into a list, in which case I can use "tolist"
rolls.tolist()
plaintext
[3, 4, 3, 4, 5, 5, 2, 1, 3, 3]

3: help() (tell me more)

python
# That "ravel" attribute sounds interesting. I'm a big classical music fan.
help(rolls.ravel)
plaintext
Help on built-in function ravel:

ravel(...) method of numpy.ndarray instance
    a.ravel([order])
    
    Return a flattened array.
    
    Refer to `numpy.ravel` for full documentation.
    
    See Also
    --------
    numpy.ravel : equivalent function
    
    ndarray.flat : a flat iterator on the array.
python
# Okay, just tell me everything there is to know about numpy.ndarray
# (Click the "output" button to see the novel-length output)
help(rolls)

太长了不愿意复制粘贴了。。。

Operator overloading

下面表达式的值是什么?

py
[3, 4, 1, 2, 2, 1] + 10
python
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_19/2144087748.py in <module>
----> 1 [3, 4, 1, 2, 2, 1] + 10

TypeError: can only concatenate list (not "int") to list

多傻的问题。当然这是一个错误。

但是……

python
rolls + 10
python
array([13, 14, 13, 14, 15, 15, 12, 11, 13, 13])

我们可能会认为,Python严格规定了其核心语法的行为方式,例如+、<、in、==或用于索引和切片的方括号。但事实上,它采取了一种不干涉的方式。定义新类型时,可以选择它的加法运算方式,或者该类型的对象等于其他值意味着什么。

列表的设计者认为,不允许将列表与数字相加。numpy数组的设计者则采取了不同的方式(为数组的每个元素添加数字)。

下面是numpy数组如何与Python操作符进行意外交互的几个例子(至少与列表不同)。

python
# At which indices are the dice less than or equal to 3?
rolls <= 3
python
array([ True, False,  True, False, False, False,  True,  True,  True,
        True])
python
xlist = [[1,2,3],[2,4,6],]
# Create a 2-dimensional array
x = numpy.asarray(xlist)
print("xlist = {}\nx =\n{}".format(xlist, x))
py
xlist = [[1, 2, 3], [2, 4, 6]]
x =
[[1 2 3]
 [2 4 6]]
python
# Get the last element of the second row of our numpy array
x[1,-1]
plaintext
6
python
# Get the last element of the second sublist of our nested list?
xlist[1,-1]
python
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_19/3020169379.py in <module>
      1 # Get the last element of the second sublist of our nested list?
----> 2 xlist[1,-1]

TypeError: list indices must be integers or slices, not tuple

Numpy的ndarray类型专门用于处理多维数据,因此它定义了自己的索引逻辑,允许我们通过元组来指定每个维度的索引。

When does 1 + 1 not equal 2?

还有比这更奇怪的事情。你可能听说过(甚至使用过)tensorflow,这是一个广泛用于深度学习的Python库。它大量使用运算符重载。

python
import tensorflow as tf
# Create two constants, each with value 1
a = tf.constant(1)
b = tf.constant(1)
# Add them together to get...
a + b
python
<tf.Tensor: shape=(), dtype=int32, numpy=2>

A + b不是2,而是(引用tensorflow文档)……

一个操作输出的符号句柄。它并不保存该操作输出的值,而是提供了一种在TensorFlow tf.Session中计算这些值的方法。

重要的是要意识到这样一个事实:这种事情是可能的,而且库经常会以一种不明显或看似神奇的方式使用运算符重载。

理解Python操作符应用于整型、字符串和列表时的工作原理,并不能保证您能够立即理解它们应用于tensorflow张量、numpy ndarray或pandas DataFrame时的工作原理。

例如,一旦你对DataFrames有了一点了解,下面这样的表达式就开始看起来很直观:

python
# Get the rows with population over 1m in South America
df[(df['population'] > 10**6) & (df['continent'] == 'South America')]

但是它为什么会起作用呢?上面的例子提供了5个不同的重载操作符。这些操作的作用是什么?当事情开始出错时,它可以帮助你知道答案。

Curious how it all works?

你是否曾经在一个对象上调用过help()或dir(),并想知道这些带有双下划线的名称到底是什么?

python
print(dir(list))
plaintext
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

事实证明,这与运算符重载直接相关。

当Python程序员想要定义操作符对其类型的行为时,他们通过实现以2个下划线开头和结尾的特殊名称的方法来实现,例如__lt__、setattr__或__contains。通常,遵循这种双下划线格式的名称对Python有特殊的含义。

例如,[1,2,3]中的表达式x实际上在幕后调用了列表方法__contains__。它等价于(丑陋得多的那个)[1,2,3].contains(x)。

如果你想了解更多,可以查看Python的官方文档,其中描述了很多很多这种特殊的“下划线”方法。

我们不会在这几节课中定义我们自己的类型(如果有时间的话!),但我希望你能在以后的道路上体验到定义自己的奇妙、怪异类型的乐趣。

Exercise:

Question 1

在完成列表和元组的练习后,Jimmy注意到,根据他的estimate_average_slot_payout函数,Learn Python赌场的老虎机实际上是被操纵的,从长远来看是有利可图的。

从口袋里的200美元开始,吉米已经玩了500次老虎机,每次旋转后都将他的新余额记录在一个列表中。他使用Python的matplotlib库绘制了平衡随时间变化的图表:

python
# Import the jimmy_slots submodule
from learntools.python import jimmy_slots
# Call the get_graph() function to get Jimmy's graph
graph = jimmy_slots.get_graph()
graph

image-20240726153728073

如你所见,他最近运气不太好。他想在推特上加上一些精选的表情符号,但就目前来看,他的粉丝可能会感到困惑。他问你是否可以帮他做以下修改:

  • 添加标题“抽500次老虎机的结果”。
  • 让y轴从0开始。
  • 在y轴上添加标签“Balance”

调用type(graph)后,可以看到Jimmy绘制的图形的类型是matplotlib.axis._subplots.axessubplot。嗯,这是一个新的类型。通过调用dir(graph),你会发现三个看起来很有用的方法:.set_title()、。set_ylim()和。set_ylabel()。

根据Jimmy的要求,使用这些方法完成函数prettify_graph。我们已经帮你完成了第一个请求(设置标题)。

(记住:如果你不知道这些方法的作用,请使用help()函数!)

python
def prettify_graph(graph):
    """Modify the given graph according to Jimmy's requests: add a title, make the y-axis
    start at 0, label the y-axis. (And, if you're feeling ambitious, format the tick marks
    as dollar amounts using the "$" symbol.)
    """
    graph.set_title("Results of 500 slot machine pulls")
    graph.set_ylim(bottom=0)
    graph.set_ylabel("Balance")
    # Complete steps 2 and 3 here

graph = jimmy_slots.get_graph()
prettify_graph(graph)
graph

Question 2

这是一个非常具有挑战性的问题。别忘了你可以得到提示!

Luigi正在尝试进行分析,以确定在马里奥赛车赛道上赢得比赛的最佳道具。他有一些字典列表形式的数据,如下所示:

[ {‘name’: ‘Peach’, ‘items’: [‘green shell’, ‘banana’, ‘green shell’,], ‘finish’: 3}, {‘name’: ‘Bowser’, ‘items’: [‘green shell’,], ‘finish’: 1},

# Sometimes the racer’s name wasn’t recorded

{‘name’: None, ‘items’: [‘mushroom’,], ‘finish’: 2}, {‘name’: ‘Toad’, ‘items’: [‘green shell’, ‘mushroom’], ‘finish’: 1}, ]

“items”是赛车手在比赛中获得的所有能量道具列表,“finish”是他们在比赛中的位置(1代表第一名,3代表第三名,以此类推)。

他编写了下面的函数,接收这样一个列表,并返回一个字典,将每个元素映射为获得第一名的次数。

python
def best_items(racers):
    """Given a list of racer dictionaries, return a dictionary mapping items to the number
    of times those items were picked up by racers who finished in first place.
    """
    winner_item_counts = {}
    for i in range(len(racers)):
        # The i'th racer dictionary
        racer = racers[i]
        # We're only interested in racers who finished in first
        if racer['finish'] == 1:
            for i in racer['items']:
                # Add one to the count for this item (adding it to the dict if necessary)
                if i not in winner_item_counts:
                    winner_item_counts[i] = 0
                winner_item_counts[i] += 1

        # Data quality issues :/ Print a warning about racers with no name set. We'll take care of it later.
        if racer['name'] is None:
            print("WARNING: Encountered racer with unknown name on iteration {}/{} (racer = {})".format(
                i+1, len(racers), racer['name'])
                 )
    return winner_item_counts

他在上面的一个小示例列表上进行了尝试,似乎正常工作了:

python
sample = [
    {'name': 'Peach', 'items': ['green shell', 'banana', 'green shell',], 'finish': 3},
    {'name': 'Bowser', 'items': ['green shell',], 'finish': 1},
    {'name': None, 'items': ['mushroom',], 'finish': 2},
    {'name': 'Toad', 'items': ['green shell', 'mushroom'], 'finish': 1},
]
best_items(sample)

然而,当他尝试在他的完整数据集上运行它时,程序崩溃并报出TypeError。

你能猜到为什么吗?试着运行下面的代码单元格,看看Luigi得到的错误消息。一旦你确定了bug,在下面的单元格中修复它(这样它就可以正常运行了)。

提示:Luigi的bug类似于我们在教程中讨论star导入时遇到的bug。

Error Code:

python
# Import luigi's full dataset of race data
from learntools.python.luigi_analysis import full_dataset

# Fix me!
def best_items(racers):
    winner_item_counts = {}
    for i in range(len(racers)):
        # The i'th racer dictionary
        racer = racers[i]
        # We're only interested in racers who finished in first
        if racer['finish'] == 1:
            for i in racer['items']:
                # Add one to the count for this item (adding it to the dict if necessary)
                if i not in winner_item_counts:
                    winner_item_counts[i] = 0
                winner_item_counts[i] += 1

        # Data quality issues :/ Print a warning about racers with no name set. We'll take care of it later.
        if racer['name'] is None:
            print("WARNING: Encountered racer with unknown name on iteration {}/{} (racer = {})".format(
                i+1, len(racers), racer['name'])
                 )
    return winner_item_counts

# Try analyzing the imported full dataset
best_items(full_dataset)

Corrotr Code:

python
# Import luigi's full dataset of race data
from learntools.python.luigi_analysis import full_dataset

# Fix me!
def best_items(racers):
    winner_item_counts = {}
    for i in range(len(racers)):
        # The i'th racer dictionary
        racer = racers[i]
        # We're only interested in racers who finished in first
        if racer['finish'] == 1:
            for j in racer['items']:
                # Add one to the count for this item (adding it to the dict if necessary)
                if j not in winner_item_counts:
                    winner_item_counts[i] = 0
                winner_item_counts[i] += 1

        # Data quality issues :/ Print a warning about racers with no name set. We'll take care of it later.
        if racer['name'] is None:
            print("WARNING: Encountered racer with unknown name on iteration {}/{} (racer = {})".format(
                i+1, len(racers), racer['name'])
                 )
    return winner_item_counts

# Try analyzing the imported full dataset
best_items(full_dataset)

Question 3

假设我们想创建一个新类型来表示blackjack中的手牌。我们可能想对这种类型做的一件事是重载比较操作符,如>和<=,以便我们可以使用它们来检查一只手是否打败另一只手。如果我们可以这样做,那就太酷了:

python
>>> hand1 = BlackjackHand(['K', 'A'])
>>> hand2 = BlackjackHand(['7', '10', 'A'])
>>> hand1 > hand2
True

好吧,我们不会在这个问题中做所有这些(定义自定义类有点超出了这些课程的范围),但是我们要求您在下面的函数中编写的代码与我们定义自己的BlackjackHand类时必须编写的代码非常相似。(我们把它放在__gt__魔术方法中,为>定义我们的自定义行为 >。)

根据文档字符串填充blackjack_hand_greater_than函数的函数体。

python
def hand_total(hand):
    """Helper function to calculate the total points of a blackjack hand.
    """
    total = 0
    # Count the number of aces and deal with how to apply them at the end.
    aces = 0
    for card in hand:
        if card in ['J', 'Q', 'K']:
            total += 10
        elif card == 'A':
            aces += 1
        else:
            # Convert number cards (e.g. '7') to ints
            total += int(card)
    # At this point, total is the sum of this hand's cards *not counting aces*.

    # Add aces, counting them as 1 for now. This is the smallest total we can make from this hand
    total += aces
    # "Upgrade" aces from 1 to 11 as long as it helps us get closer to 21
    # without busting
    while total + 10 <= 21 and aces > 0:
        # Upgrade an ace from 1 to 11
        total += 10
        aces -= 1
    return total

def blackjack_hand_greater_than(hand_1, hand_2):
    total_1 = hand_total(hand_1)
    total_2 = hand_total(hand_2)
    return total_1 <= 21 and (total_1 > total_2 or total_2 > 21)